package checker

import (
	"mi/compiler/ast"
	"mi/compiler/diagnostic"
)

// TYPE CHECKING
func (c *checker) isTypeIdenticalTo(source Type, target Type) bool {
	return c.checkTypeRelatedTo(source, target, nil, c.identityRelation, nil, nil) != noMatch
}

func (c *checker) isTypeSubtypeOf(source Type, target Type) bool {
	return c.checkTypeSubtypeOf(source, target, nil, nil)
}

func (c *checker) checkTypeSubtypeOf(source Type, target Type, errorNode ast.Node, diagnosticMessage *diagnostic.Message) bool {
	return c.checkTypeRelatedTo(source, target, nil, c.subtypeRelation, errorNode, diagnosticMessage) != noMatch
}

func (c *checker) isTypeAssignableTo(source Type, target Type, constantValue *Number) bool {
	return c.checkTypeAssignableTo(source, target, constantValue, nil, nil) != noMatch
}

// This is *not* a bi-directional relationship.
// If one needs to check both directions for comparability, use a second call to this function or 'checkTypeComparableTo'.
func (c *checker) isTypeComparableTo(source Type, target Type) bool {
	return c.checkTypeComparableTo(source, target, nil, nil) != noMatch
}

func (c *checker) areTypesComparable(type1 Type, type2 Type) bool {
	return c.isTypeComparableTo(type1, type2) || c.isTypeComparableTo(type2, type1)
}

func (c *checker) checkTypeAssignableTo(source Type, target Type, constantValue *Number, errorNode ast.Node, diagnosticMessage *diagnostic.Message) int {
	return c.checkTypeRelatedTo(source, target, constantValue, c.assignableRelation, errorNode, diagnosticMessage)
}

// This is *not* a bi-directional relationship.
// If one needs to check both directions for comparability, use a second call to this function or 'isTypeComparableTo'.
func (c *checker) checkTypeComparableTo(source Type, target Type, errorNode ast.Node, diagnosticMessage *diagnostic.Message) int {
	return c.checkTypeRelatedTo(source, target, nil, c.comparableRelation, errorNode, diagnosticMessage)
}

func (c *checker) isTypeRelatedTo(source Type, target Type, constantValue *Number, relation *relatedCache) int {
	return c.checkTypeRelatedTo(source, target, constantValue, relation, nil, nil)
}

const noMatch = -1
type relatedCache struct {
	flags relationFlags
	m map[int64]int
}

type relationFlags = int
const (
	subtypeRelation  = 1 << iota
	assignableRelation
	comparableRelation
	identityRelation
)

func newRelatedCache(flags relationFlags) *relatedCache {
	return &relatedCache{
		flags: flags,
		m: make(map[int64]int),
	}
}

func (r *relatedCache) hasFlags(flags relationFlags) bool {
	return r.flags&flags != 0
}

func (r *relatedCache) contains(id1, id2 int) bool {
	var uuid = int64(id1)<<32 | int64(id2)
	_, ok := r.m[uuid]
	return ok
}

func (r *relatedCache) get(id1, id2 int) int {
	var uuid = int64(id1)<<32 | int64(id2)
	if diff, ok := r.m[uuid]; ok {
		return diff
	}

	return noMatch
}

func (r *relatedCache) set(id1, id2, difference int) {
	var uuid = int64(id1)<<32 | int64(id2)
	r.m[uuid] = difference
}

func (c *checker) checkTypeRelatedTo(source Type, target Type, constantValue *Number, relation *relatedCache, errorNode ast.Node, diagnosticMessage *diagnostic.Message) int {
	if !(relation != c.identityRelation || errorNode == nil) {
		panic("no error reporting in identity checking")
	}

	var result = c.isRelated(source, target, constantValue, relation, false)
	if !isNull(errorNode) && result == noMatch {
		if isNull(diagnosticMessage) {
			diagnosticMessage = diagnostic.M_Type_0_is_not_assignable_to_type_1
		}
		c.error(errorNode, diagnosticMessage,
			c.typeToString(source, nil, true),
			c.typeToString(target, nil, true))
	}
	return result
}

const char2short 	= 8 + 1
const int2byte 		= 8 + 3
const int2char 		= 8 + 2
const int2short 	= 8 + 2
func (c *checker) constantAssignmentConversion(source Type, target Type, constantValue *Number, relation *relatedCache) int {
	if isNull(constantValue) {
		return noMatch
	}

	if source.Flags()&(TFInt|TFChar) == 0 {
		return noMatch
	}

	if relation != c.assignableRelation {
		return noMatch
	}

	if relation.get(source.ID(), target.ID()) != noMatch {
		return noMatch
	}

	// If the value of a character constant is in the range of short, it can be assigned
	if source.Flags()&TFChar != 0 && target.Flags()&TFShort != 0 && inValueRange(TFShort, constantValue.value){
		return char2short
	}

	// If the integer value range is appropriate, it can be assigned to byte, character, short
	if source.Flags()&TFInt != 0 {
		if target.Flags()&TFByte != 0 && inValueRange(TFByte, constantValue.value) {
			return int2byte
		}
		if target.Flags()&TFChar != 0 && inValueRange(TFChar, constantValue.value) {
			return int2char
		}
		if target.Flags()&TFShort != 0 && inValueRange(TFShort, constantValue.value) {
			return int2short
		}
	}

	return noMatch
}

// inTypeArgumentRelated Is a context mark, When in this context, the relation of the type
// parameter is no longer required to be identity, allowing subtype assignment of bounds type
//
// The following describes the assignable relation
//
// We have two classes A and B, B extends A
// 		class A {}
// 		class B extends A {}
// 		class List<T> {}
//
// If none of the type parameters are a bound type, then the type parameters must be equal
// 	 	List<B> = List<A>   			X
//
// Because both A and B are in the scope of '? extends A', they are allowed
// 		List<? extends A> = List<A> 	√
// 		List<? extends A> = List<B> 	√
//
// Because both A and B are in the scope of '? extends A', they are allowed
// 		List<? super B> = List<A> 	√
// 		List<? super B> = List<B> 	√
//
// Because '? extends B' are in the scope of '? extends A', they are allowed
// 		List<? extends A> = List<? extends B> √
//
// Because '? super A' are in the scope of '? super B', they are allowed
// 		List<? super B> = List<? extends A> √
//
// This rule does not allow nesting
// 		List<List<? extends A>> =  List<List<B>>  X
func (c *checker) isRelated(source Type, target Type, constantValue *Number, relation *relatedCache, inTypeArgumentRelated bool) int {
	if source == c.intType && target == c.charType {
		_ = source
	}

	if diff := c.constantAssignmentConversion(source, target, constantValue, relation); diff != noMatch {
		return diff
	}

	if relation.contains(source.ID(), target.ID()) {
		return relation.get(source.ID(), target.ID())
	}

	var diff = c.isRelatedWorker(source, target, relation, inTypeArgumentRelated)
	relation.set(source.ID(), target.ID(), diff)
	return diff
}

func (c *checker) isRelatedWorker(source Type, target Type, relation *relatedCache, inTypeArgumentRelated bool) int {
	// Contains the case of identity
	// If the basic data type is consistent, it will match here
	if source == target {
		return 0
	}

	// If source or target contains any type, return
	// true, they should have reported errors elsewhere
	if source.Flags()&TFAny != 0 || target.Flags()&TFAny != 0 {
		// set a value to avoid conflicts with other
		// correct types when overloading decisions
		return 1000
	}

	if !relation.hasFlags(identityRelation) {

		// Similar values are allowed to be compared
		if relation.hasFlags(comparableRelation)  {
			if source.hasFlags(TFNumberLike) && target.hasFlags(TFNumberLike) {
				return 0
			}
		}

		if source.Flags()&TFReference != 0 && target.Flags()&TFReference != 0 {
			if source.(TypeReference).Target() == target.(TypeReference).Target() {
				return c.typeReferenceRelatedTo(source.(TypeReference), target.(TypeReference), relation)
			}
		}

		if source.Flags()&TFBoundsLike != 0 || target.Flags()&TFBoundsLike != 0 {
			return c.boundTypeRelatedTo(source, target, relation, inTypeArgumentRelated)
		}

		if relation.hasFlags(assignableRelation|comparableRelation) && source == c.nullType &&
			target.Flags()&(TFClassLike|TFInterfaceLike|TFReference) != 0 {
			return 0
		}

		if source.Flags()&TFObjectLike != 0 && target.Flags()&TFObjectLike != 0 {
			var sourceApparentType =  c.getApparentType(source)
			var targetApparentType =  c.getApparentType(target)
			return c.objectTypeRelatedTo(sourceApparentType.(ObjectType), targetApparentType.(ObjectType), relation)
		}
	}

	return noMatch
}

func (c *checker) isTypeArgumentRelatedTo(source Type, target Type, relation *relatedCache) int {
	if relation.hasFlags(assignableRelation) {
		if source.Flags()&(TFClassLike|TFInterfaceLike) != 0 &&
			target.Flags()&(TFClassLike|TFInterfaceLike) != 0 {
			return c.isRelated(source, target, nil, c.identityRelation, false)
		}
		if target.Flags()&TFBoundsLike != 0 {
			return c.isRelated(source, target, nil, c.assignableRelation, true)
		}
	}

	if relation.hasFlags(subtypeRelation|comparableRelation) {
		if source.Flags()&TFBoundsLike != 0 && target.Flags()&TFBoundsLike != 0 {
			return c.isRelated(source, target, nil, c.subtypeRelation, true)
		}
	}

	return noMatch
}

func (c *checker) typeReferenceRelatedTo(source TypeReference, target TypeReference, relation *relatedCache) int {
	if source.Target() == c.globalArrayType {
		return c.isRelated(source.TypeArguments()[0], target.TypeArguments()[0], nil, relation, false)
	}

	var difference = 0
	for i := 0; i < len(source.TypeArguments()); i++ {
		var sourceTypeArgument = source.TypeArguments()[i]
		var targetTypeArgument = target.TypeArguments()[i]
		if diff := c.isTypeArgumentRelatedTo(sourceTypeArgument, targetTypeArgument, relation); diff == noMatch  {
			return diff
		} else {
			difference += diff
		}
	}
	return difference
}

func (c *checker) boundTypeRelatedTo(source Type, target Type, relation *relatedCache, inTypeArgumentRelated bool) int {
	if inTypeArgumentRelated {
		if relation.hasFlags(assignableRelation|subtypeRelation) {
			if inTypeArgumentRelated && target.Flags()&TFNoBounds != 0 {
				return 0
			}
			if inTypeArgumentRelated && source.Flags()&TFInBounds != 0 && target.Flags()&TFInBounds != 0 {
				var sourceOrg = source.(*inBoundsType).target
				var targetOrg = target.(*inBoundsType).target
				return c.isRelated(targetOrg, sourceOrg, nil, c.subtypeRelation, false)
			}
			if inTypeArgumentRelated && source.Flags()&TFOutBounds != 0 && target.Flags()&TFOutBounds != 0 {
				var sourceOrg = source.(*outBoundsType).target
				var targetOrg = target.(*outBoundsType).target
				return c.isRelated(sourceOrg, targetOrg, nil, c.subtypeRelation, false)
			}
		}
		if relation.hasFlags(assignableRelation) {
			if source.Flags()&(TFClassLike|TFInterfaceLike|TFReference) != 0 && target.Flags()&TFOutBounds != 0 {
				var targetOrg = target.(*outBoundsType).target
				return c.isRelated(source, targetOrg, nil, c.subtypeRelation, false)
			}
			if source.Flags()&(TFClassLike|TFInterfaceLike|TFReference) != 0 && target.Flags()&TFInBounds != 0 {
				var targetOrg = target.(*inBoundsType).target
				return c.isRelated(targetOrg, source, nil, c.subtypeRelation, false)
			}
		}
	} else {
		if relation.hasFlags(assignableRelation) {
			if (source.Flags()&(TFClassLike|TFInterfaceLike|TFReference) != 0 || source == c.nullType) && target.Flags()&TFInBounds != 0 {
				var targetOrg = target.(*inBoundsType).target
				return c.isRelated(source, targetOrg, nil, c.subtypeRelation, false)
			}
			if source.Flags()&TFOutBounds != 0 && target.Flags()&(TFClassLike|TFInterfaceLike|TFReference) != 0 {
				var sourceOrg = source.(*outBoundsType).target
				return c.isRelated(sourceOrg, target, nil, c.subtypeRelation, false)
			}
		}
	}

	return noMatch
}

func (c *checker) getMaxInheritedDepth(tpe ObjectType, base ObjectType) int {
	var max = -1
	var count func(level int, cur Type)
	count = func(level int, cur Type) {
		if cur == base {
			if level > max {
				max = level
			}
			return
		}

		var baseTypes []BaseType
		if cur.hasFlags(TFClassLike|TFInterfaceLike) {
			baseTypes = c.getBaseTypes(cur.(InterfaceType))
		} else if cur.hasFlags(TFReference) {
			baseTypes = c.getBaseTypesOfTypeReference(cur.(TypeReference))
		}
		for _, baseType := range baseTypes {
			count(level + 1, baseType.Type)
		}
	}

	count(1, tpe)
	return max
}

func (c *checker) objectTypeRelatedTo(source ObjectType, target ObjectType, relation *relatedCache) int {
	if relation.hasFlags(assignableRelation|subtypeRelation|comparableRelation) {
		if source.Flags()&(TFClassLike|TFInterfaceLike|TFReference) != 0 &&
			target.Flags()&(TFClassLike|TFInterfaceLike|TFReference) != 0{
			return c.getMaxInheritedDepth(source, target)
		}
	}

	if relation.hasFlags(assignableRelation) {
		if source.Flags()&TFAnonymous != 0 &&
			c.isTypeFromArrowFunction(source) && c.isFunctionalInterface(target) {
			return c.isArrowFunctionRelatedToFunctionalInterface(source, target)
		}
	}


	return noMatch
}

func (c *checker) isArrowFunctionRelatedToFunctionalInterface(source ObjectType, target ObjectType) int {
	var sourceSignatures = c.getSignaturesOfType(source, SIKCall)
	var targetSignature = c.getFunctionInterfaceSignature(target)

	if len(sourceSignatures) != 1 {
		return noMatch
	}

	var sourceSignature = sourceSignatures[0]

	if sourceSignature == targetSignature {
		return 0
	}

	if !(len(sourceSignature.Parameters) == 0 &&
		len(targetSignature.Parameters) == 1){
		if sourceSignature.HasRestParameter != targetSignature.HasRestParameter {
			return noMatch
		}

		if len(sourceSignature.Parameters) != len(targetSignature.Parameters) {
			return noMatch
		}

		if sourceSignature.MinArgumentCount != targetSignature.MinArgumentCount {
			return noMatch
		}

		if sourceSignature.TypeParameters != nil && targetSignature.TypeParameters != nil {
			if len(sourceSignature.TypeParameters) != len(targetSignature.TypeParameters) {
				return noMatch
			}

			for i := 0; i < len(sourceSignature.TypeParameters); i++ {
				if !c.isTypeIdenticalTo(sourceSignature.TypeParameters[i], targetSignature.TypeParameters[i]) {
					return noMatch
				}
			}
		} else if sourceSignature.TypeParameters != nil || targetSignature.TypeParameters != nil {
			return noMatch
		}

		// Spec 1.0 Section 3.8.3 & 3.8.4:
		// M and N (the signatures) are instantiated using type Any as the type argument for all type parameters declared by M and N
		sourceSignature = c.getErasedSignature(sourceSignature)
		targetSignature = c.getErasedSignature(targetSignature)
		for i := 0; i < len(sourceSignature.Parameters); i++ {
			var s Type
			if sourceSignature.HasRestParameter && i == len(sourceSignature.Parameters)-1 {
				s = c.getRestTypeOfSignature(sourceSignature)
			} else {
				s = c.getTypeOfSymbol(sourceSignature.Parameters[i])
			}
			var t Type
			if targetSignature.HasRestParameter && i == len(sourceSignature.Parameters)-1 {
				t = c.getRestTypeOfSignature(targetSignature)
			} else {
				t = c.getTypeOfSymbol(targetSignature.Parameters[i])
			}
			if !c.isTypeIdenticalTo(s, t) {
				return noMatch
			}
		}
	}

	var t = c.getReturnTypeOfSignature(targetSignature)
	var s = c.getReturnTypeOfSignature(sourceSignature)
	if c.isTypeAssignableTo(s, t, nil) {
		return 0
	}

	return noMatch
}